// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

//! Tile set is a special storage for tile descriptions. It is a sort of database, that contains
//! descriptions (definitions) for tiles. See [`TileSet`] docs for more info and usage examples.

use crate::scene::base::Property;
use crate::{
    asset::{
        io::ResourceIo,
        loader::{BoxedLoaderFuture, LoaderPayload, ResourceLoader},
        manager::ResourceManager,
        state::LoadError,
        Resource, ResourceData,
    },
    core::{
        algebra::Vector2,
        color::Color,
        io::FileLoadError,
        math::Rect,
        pool::{Handle, Pool},
        reflect::prelude::*,
        type_traits::prelude::*,
        visitor::prelude::*,
    },
    material::MaterialResource,
};
use fxhash::FxHashSet;
use std::{
    any::Any,
    error::Error,
    fmt::{Display, Formatter},
    path::{Path, PathBuf},
    sync::Arc,
};
use strum_macros::{AsRefStr, EnumString, VariantNames};

/// An error that may occur during tile set resource loading.
#[derive(Debug)]
pub enum TileSetResourceError {
    /// An i/o error has occurred.
    Io(FileLoadError),

    /// An error that may occur due to version incompatibilities.
    Visit(VisitError),
}

impl Display for TileSetResourceError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Io(v) => {
                write!(f, "A file load error has occurred {v:?}")
            }
            Self::Visit(v) => {
                write!(
                    f,
                    "An error that may occur due to version incompatibilities. {v:?}"
                )
            }
        }
    }
}

impl From<FileLoadError> for TileSetResourceError {
    fn from(e: FileLoadError) -> Self {
        Self::Io(e)
    }
}

impl From<VisitError> for TileSetResourceError {
    fn from(e: VisitError) -> Self {
        Self::Visit(e)
    }
}

/// Supported collider types for tiles.
#[derive(
    Copy,
    Clone,
    Hash,
    PartialEq,
    Eq,
    Debug,
    Default,
    Visit,
    Reflect,
    AsRefStr,
    EnumString,
    VariantNames,
    TypeUuidProvider,
)]
#[type_uuid(id = "04a44fec-394f-4497-97d5-fe9e6f915831")]
pub enum TileCollider {
    /// No collider.
    None,
    /// Rectangle collider.
    #[default]
    Rectangle,
    /// Mesh collider, the mesh is autogenerated.
    Mesh,
}

/// Definition of a tile.
#[derive(Clone, Default, Debug, Reflect, Visit)]
#[visit(optional)]
pub struct TileDefinition {
    /// Material of the tile.
    pub material: MaterialResource,
    /// A rectangle, that defines texture coordinates range for the tile.
    pub uv_rect: Rect<f32>,
    /// Collider of the tile.
    pub collider: TileCollider,
    /// Color of the tile.
    pub color: Color,
    /// Position of the tile, it is used **only** to be able to construct brushes from tile sets.
    pub position: Vector2<i32>,
    /// A custom set of properties. Properties could be used to assign additional information for
    /// tiles, such surface type (for example, lava, ice, dirt, etc.), physics properties and so
    /// on.
    pub properties: Vec<Property>,
}

/// An alias for `Handle<TileDefinition>`.
pub type TileDefinitionHandle = Handle<TileDefinition>;

/// Tile set is a special storage for tile descriptions. It is a sort of database, that contains
/// descriptions (definitions) for tiles. Such approach allows you to change appearance of all tiles
/// of particular kind at once.
#[derive(Clone, Default, Debug, Reflect, Visit, TypeUuidProvider, ComponentProvider)]
#[type_uuid(id = "7b7e057b-a41e-4150-ab3b-0ae99f4024f0")]
pub struct TileSet {
    /// Tiles of the tile set.
    pub tiles: Pool<TileDefinition>,
}

impl TileSet {
    /// Adds a tile definition to the tile set and returns its handle.
    pub fn add_tile(&mut self, tile: TileDefinition) -> TileDefinitionHandle {
        self.tiles.spawn(tile)
    }

    /// Load a tile set resource from the specific file path.
    pub async fn from_file(
        path: &Path,
        resource_manager: ResourceManager,
        io: &dyn ResourceIo,
    ) -> Result<Self, TileSetResourceError> {
        let bytes = io.load_file(path).await?;
        let mut visitor = Visitor::load_from_memory(&bytes)?;
        visitor.blackboard.register(Arc::new(resource_manager));
        let mut tile_set = TileSet::default();
        tile_set.visit("TileSet", &mut visitor)?;
        Ok(tile_set)
    }

    /// Tries to find a tile definition with the given position.
    pub fn find_tile_at_position(&self, position: Vector2<i32>) -> Option<TileDefinitionHandle> {
        for (handle, tile) in self.tiles.pair_iter() {
            if tile.position == position {
                return Some(handle);
            }
        }
        None
    }

    /// Tries to find free location at the given position. It uses brute-force searching algorithm
    /// and could be slow if called dozens of time per frame or on a large tile set.
    pub fn find_free_location(&self, position: Vector2<i32>) -> Vector2<i32> {
        let mut visited = FxHashSet::default();
        let mut stack = vec![position];
        while let Some(pos) = stack.pop() {
            if visited.contains(&pos) {
                continue;
            }
            visited.insert(pos);
            if self.find_tile_at_position(pos).is_none() {
                return pos;
            } else {
                stack.extend_from_slice(&[
                    Vector2::new(pos.x + 1, pos.y),
                    Vector2::new(pos.x + 1, pos.y + 1),
                    Vector2::new(pos.x, pos.y + 1),
                    Vector2::new(pos.x - 1, pos.y + 1),
                    Vector2::new(pos.x - 1, pos.y),
                    Vector2::new(pos.x - 1, pos.y - 1),
                    Vector2::new(pos.x, pos.y - 1),
                    Vector2::new(pos.x + 1, pos.y - 1),
                ]);
            }
        }
        Default::default()
    }
}

impl ResourceData for TileSet {
    fn as_any(&self) -> &dyn Any {
        self
    }

    fn as_any_mut(&mut self) -> &mut dyn Any {
        self
    }

    fn type_uuid(&self) -> Uuid {
        <Self as TypeUuidProvider>::type_uuid()
    }

    fn save(&mut self, path: &Path) -> Result<(), Box<dyn Error>> {
        let mut visitor = Visitor::new();
        self.visit("TileSet", &mut visitor)?;
        visitor.save_binary(path)?;
        Ok(())
    }

    fn can_be_saved(&self) -> bool {
        true
    }
}

/// An alias for `Resource<TileSet>`.
pub type TileSetResource = Resource<TileSet>;

/// Standard tile set resource loader.
pub struct TileSetLoader {
    /// Resource manager of the engine.
    pub resource_manager: ResourceManager,
}

impl ResourceLoader for TileSetLoader {
    fn extensions(&self) -> &[&str] {
        &["tileset"]
    }

    fn data_type_uuid(&self) -> Uuid {
        <TileSet as TypeUuidProvider>::type_uuid()
    }

    fn load(&self, path: PathBuf, io: Arc<dyn ResourceIo>) -> BoxedLoaderFuture {
        let resource_manager = self.resource_manager.clone();
        Box::pin(async move {
            let tile_set = TileSet::from_file(&path, resource_manager, io.as_ref())
                .await
                .map_err(LoadError::new)?;
            Ok(LoaderPayload::new(tile_set))
        })
    }
}
